// Copyright 2021 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This file provides core interop helpers used by the code generated by the
// templates.

#ifndef SRC_DAWN_NODE_INTEROP_CORE_H_
#define SRC_DAWN_NODE_INTEROP_CORE_H_

#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>

#include "src/dawn/node/interop/Napi.h"
#include "src/dawn/node/utils/Debug.h"

#define ENABLE_INTEROP_LOGGING 0  // Enable for verbose interop logging

#if ENABLE_INTEROP_LOGGING
#define INTEROP_LOG(...) LOG(__VA_ARGS__)
#else
#define INTEROP_LOG(...)
#endif

// A helper macro for constructing a PromiseInfo with the current file, function and line.
// See PromiseInfo
#define PROMISE_INFO                     \
    ::wgpu::interop::PromiseInfo {       \
        __FILE__, __FUNCTION__, __LINE__ \
    }

namespace wgpu::interop {

////////////////////////////////////////////////////////////////////////////////
// Primitive JavaScript types
////////////////////////////////////////////////////////////////////////////////
using Object = Napi::Object;
using ArrayBuffer = Napi::ArrayBuffer;
using Int8Array = Napi::TypedArrayOf<int8_t>;
using Int16Array = Napi::TypedArrayOf<int16_t>;
using Int32Array = Napi::TypedArrayOf<int32_t>;
using Uint8Array = Napi::TypedArrayOf<uint8_t>;
using Uint16Array = Napi::TypedArrayOf<uint16_t>;
using Uint32Array = Napi::TypedArrayOf<uint32_t>;
using Float32Array = Napi::TypedArrayOf<float>;
using Float64Array = Napi::TypedArrayOf<double>;
using DataView = Napi::TypedArray;

// Datatype used for undefined values.
struct UndefinedType {};
static constexpr UndefinedType Undefined;

template <typename T>
using FrozenArray = std::vector<T>;

// A wrapper class for integers that's as transparent as possible and is used to distinguish
// that the type is tagged with the [Clamp] WebIDL attribute.
template <typename T>
struct ClampedInteger {
    static_assert(std::is_integral_v<T>);

    using IntegerType = T;
    ClampedInteger() : value(0) {}
    // NOLINTNEXTLINE(runtime/explicit)
    ClampedInteger(T value) : value(value) {}
    operator T() const { return value; }
    T value;
};

// A wrapper class for integers that's as transparent as possible and is used to distinguish
// that the type is tagged with the [EnforceRange] WebIDL attribute.
template <typename T>
struct EnforceRangeInteger {
    static_assert(std::is_integral_v<T>);

    using IntegerType = T;
    EnforceRangeInteger() : value(0) {}
    // NOLINTNEXTLINE(runtime/explicit)
    EnforceRangeInteger(T value) : value(value) {}
    operator T() const { return value; }
    T value;
};

////////////////////////////////////////////////////////////////////////////////
// Result
////////////////////////////////////////////////////////////////////////////////

// Result is used to hold an success / error state by functions that perform JS <-> C++
// conversion
struct [[nodiscard]] Result {
    // Returns true if the operation succeeded, false if there was an error
    inline operator bool() const { return error.empty(); }

    // If Result is an error, then a new Error is returned with the
    // stringified values append to the error message.
    // If Result is a success, then a success Result is returned.
    template <typename... VALUES>
    Result Append(VALUES&&... values) {
        if (*this) {
            return *this;
        }
        std::stringstream ss;
        ss << error << "\n";
        utils::Write(ss, std::forward<VALUES>(values)...);
        return {ss.str()};
    }

    // The error message, if the operation failed.
    std::string error;
};

// A successful result
extern Result Success;

// Returns a Result with the given error message
Result Error(std::string msg);

////////////////////////////////////////////////////////////////////////////////
// Interface<T>
////////////////////////////////////////////////////////////////////////////////

// Interface<T> is a templated wrapper around a JavaScript object, which
// implements the template-generated interface type T. Interfaces are returned
// by either calling T::Bind() or T::Create().
template <typename T>
class Interface {
  public:
    // Constructs an Interface with no JS object.
    inline Interface() {}

    // Constructs an Interface wrapping the given JS object.
    // The JS object must have been created with a call to T::Bind().
    explicit inline Interface(Napi::Object o) : object(o) {}

    // Implicit conversion operators to Napi objects.
    inline operator napi_value() const { return object; }
    inline operator const Napi::Value&() const { return object; }
    inline operator const Napi::Object&() const { return object; }

    // Member and dereference operators
    inline T* operator->() const { return T::Unwrap(object); }
    inline T* operator*() const { return T::Unwrap(object); }

    // As<IMPL>() returns the unwrapped object cast to the implementation type.
    // The interface implementation *must* be of the template type IMPL.
    template <typename IMPL>
    inline IMPL* As() const {
        return static_cast<IMPL*>(T::Unwrap(object));
    }

  private:
    Napi::Object object;
};

////////////////////////////////////////////////////////////////////////////////
// Promise<T>
////////////////////////////////////////////////////////////////////////////////

// Info holds details about where the promise was constructed.
// Used for printing debug messages when a promise is finalized without being resolved
// or rejected.
// Use the PROMISE_INFO macro to populate this structure.
struct PromiseInfo {
    const char* file = nullptr;
    const char* function = nullptr;
    int line = 0;
};

enum class PromiseState {
    Pending,
    Resolved,
    Rejected,
    Discarded,
};

namespace detail {
// Base class for Promise<T> specializations.
class PromiseBase {
  public:
    // Implicit conversion operators to Napi promises.
    inline operator napi_value() const { return state_->deferred.Promise(); }
    inline operator Napi::Value() const { return state_->deferred.Promise(); }
    inline operator Napi::Promise() const { return state_->deferred.Promise(); }

    // Reject() rejects the promise with the given failure value.
    void Reject(Napi::Value value) const {
        state_->deferred.Reject(value);
        state_->state = PromiseState::Rejected;
    }
    void Reject(Napi::Error err) const { Reject(err.Value()); }
    void Reject(std::string err) const { Reject(Napi::Error::New(state_->deferred.Env(), err)); }

    PromiseState GetState() const { return state_->state; }

    // Place the promise into the Discarded state.
    // This state will not throw a fatal error if this promise has not been rejected or resolved by
    // the time it is finalized. This is useful for Node tear-down where the attempting to create
    // new objects will result in a fatal error.
    void Discard() { state_->state = PromiseState::Discarded; }

  protected:
    void Resolve(Napi::Value value) const {
        state_->deferred.Resolve(value);
        state_->state = PromiseState::Resolved;
    }

    struct State {
        Napi::Promise::Deferred deferred;
        PromiseInfo info;
        PromiseState state = PromiseState::Pending;
    };

    PromiseBase(Napi::Env env, const PromiseInfo& info)
        : state_(new State{Napi::Promise::Deferred::New(env), info}) {
        state_->deferred.Promise().AddFinalizer(
            [](Napi::Env, State* state) {
                if (state->state == PromiseState::Pending) {
                    ::wgpu::utils::Fatal("Promise not resolved or rejected", state->info.file,
                                         state->info.line, state->info.function);
                }
                delete state;
            },
            state_);
    }

    State* const state_;
};
}  // namespace detail

// Promise<T> is a templated wrapper around a JavaScript promise, which can
// resolve to the template type T.
template <typename T>
class Promise : public detail::PromiseBase {
  public:
    // Constructor
    Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) {}

    // Resolve() fulfills the promise with the given value.
    void Resolve(T&& value) const {
        PromiseBase::Resolve(ToJS(state_->deferred.Env(), std::forward<T>(value)));
    }
};

// Specialization for Promises that resolve with no value
template <>
class Promise<void> : public detail::PromiseBase {
  public:
    // Constructor
    Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) {}

    // Resolve() fulfills the promise.
    void Resolve() const { PromiseBase::Resolve(state_->deferred.Env().Undefined()); }
};

////////////////////////////////////////////////////////////////////////////////
// Converter<T>
////////////////////////////////////////////////////////////////////////////////

// Converter<T> is specialized for each type T which can be converted from C++
// to JavaScript, or JavaScript to C++.
// Each specialization of Converter<T> is expected to have two static methods
// with the signatures:
//
//  // FromJS() converts the JavaScript value 'in' to the C++ value 'out'.
//  static Result FromJS(Napi::Env, Napi::Value in, T& out);
//
//  // ToJS() converts the C++ value 'in' to a JavaScript value, and returns
//  // this value.
//  static Napi::Value ToJS(Napi::Env, T in);
template <typename T>
class Converter {};

template <>
class Converter<Napi::Object> {
  public:
    static inline Result FromJS(Napi::Env, Napi::Value value, Napi::Object& out) {
        if (value.IsObject()) {
            out = value.ToObject();
            return Success;
        }
        return Error("value is not an object");
    }
    static inline Napi::Value ToJS(Napi::Env, Napi::Object value) { return value; }
};

template <>
class Converter<ArrayBuffer> {
  public:
    static inline Result FromJS(Napi::Env, Napi::Value value, ArrayBuffer& out) {
        if (value.IsArrayBuffer()) {
            out = value.As<ArrayBuffer>();
            return Success;
        }
        return Error("value is not a ArrayBuffer");
    }
    static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { return value; }
};

template <>
class Converter<Napi::TypedArray> {
  public:
    static inline Result FromJS(Napi::Env, Napi::Value value, Napi::TypedArray& out) {
        if (value.IsTypedArray()) {
            out = value.As<Napi::TypedArray>();
            return Success;
        }
        return Error("value is not a TypedArray");
    }
    static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { return value; }
};

template <typename T>
class Converter<Napi::TypedArrayOf<T>> {
  public:
    // clang-format off
        // The Napi element type of T
        static constexpr napi_typedarray_type element_type =
              std::is_same<T, int8_t>::value   ? napi_int8_array
            : std::is_same<T, uint8_t>::value  ? napi_uint8_array
            : std::is_same<T, int16_t>::value  ? napi_int16_array
            : std::is_same<T, uint16_t>::value ? napi_uint16_array
            : std::is_same<T, int32_t>::value  ? napi_int32_array
            : std::is_same<T, uint32_t>::value ? napi_uint32_array
            : std::is_same<T, float>::value    ? napi_float32_array
            : std::is_same<T, double>::value   ? napi_float64_array
            : std::is_same<T, int64_t>::value  ? napi_bigint64_array
            : std::is_same<T, uint64_t>::value ? napi_biguint64_array
            : static_cast<napi_typedarray_type>(-1);
    // clang-format on
    static_assert(static_cast<int>(element_type) >= 0,
                  "unsupported T type for Napi::TypedArrayOf<T>");
    static inline Result FromJS(Napi::Env, Napi::Value value, Napi::TypedArrayOf<T>& out) {
        if (value.IsTypedArray()) {
            auto arr = value.As<Napi::TypedArrayOf<T>>();
            if (arr.TypedArrayType() == element_type) {
                out = arr;
                return Success;
            }
            return Error("value is not a TypedArray of the correct element type");
        }
        return Error("value is not a TypedArray");
    }
    static inline Napi::Value ToJS(Napi::Env, ArrayBuffer value) { return value; }
};

template <>
class Converter<std::string> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, std::string&);
    static Napi::Value ToJS(Napi::Env, std::string);
};

template <>
class Converter<bool> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, bool&);
    static Napi::Value ToJS(Napi::Env, bool);
};

template <>
class Converter<int8_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, int8_t&);
    static Napi::Value ToJS(Napi::Env, int8_t);
};

template <>
class Converter<uint8_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, uint8_t&);
    static Napi::Value ToJS(Napi::Env, uint8_t);
};

template <>
class Converter<int16_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, int16_t&);
    static Napi::Value ToJS(Napi::Env, int16_t);
};

template <>
class Converter<uint16_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, uint16_t&);
    static Napi::Value ToJS(Napi::Env, uint16_t);
};

template <>
class Converter<int32_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, int32_t&);
    static Napi::Value ToJS(Napi::Env, int32_t);
};

template <>
class Converter<uint32_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, uint32_t&);
    static Napi::Value ToJS(Napi::Env, uint32_t);
};

template <>
class Converter<int64_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, int64_t&);
    static Napi::Value ToJS(Napi::Env, int64_t);
};

template <>
class Converter<uint64_t> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, uint64_t&);
    static Napi::Value ToJS(Napi::Env, uint64_t);
};

template <>
class Converter<float> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, float&);
    static Napi::Value ToJS(Napi::Env, float);
};

template <>
class Converter<double> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, double&);
    static Napi::Value ToJS(Napi::Env, double);
};

// [Clamp]ed integers must convert values outside of the integer range by clamping them.
template <typename T>
class Converter<ClampedInteger<T>> {
  public:
    static Result FromJS(Napi::Env env, Napi::Value value, ClampedInteger<T>& out) {
        double doubleValue;
        Result res = Converter<double>::FromJS(env, value, doubleValue);
        if (!res) {
            return res;
        }

        // Check for clamping first.
        constexpr T kMin = std::numeric_limits<T>::min();
        constexpr T kMax = std::numeric_limits<T>::max();
        if (doubleValue < kMin) {
            out = kMin;
            return Success;
        }
        if (doubleValue > kMax) {
            out = kMax;
            return Success;
        }

        // Yay, no clamping! We can convert the integer type as usual.
        T correctValue;
        res = Converter<T>::FromJS(env, value, correctValue);
        if (!res) {
            return res;
        }
        out = correctValue;
        return Success;
    }
    static Napi::Value ToJS(Napi::Env env, const ClampedInteger<T>& value) {
        return Converter<T>::ToJS(env, value.value);
    }
};

// [EnforceRange] integers cause a TypeError when converted from out of range values
template <typename T>
class Converter<EnforceRangeInteger<T>> {
  public:
    static Result FromJS(Napi::Env env, Napi::Value value, EnforceRangeInteger<T>& out) {
        double doubleValue;
        Result res = Converter<double>::FromJS(env, value, doubleValue);
        if (!res) {
            return res;
        }

        // Check for out of range and throw a type error.
        constexpr double kMin = static_cast<double>(std::numeric_limits<T>::min());
        constexpr double kMax = static_cast<double>(std::numeric_limits<T>::max());
        if (!(kMin <= doubleValue && doubleValue <= kMax)) {
            return Error("Values are out of the range of that integer.");
        }

        // Yay, no error! We can convert the integer type as usual.
        T correctValue;
        res = Converter<T>::FromJS(env, value, correctValue);
        if (!res) {
            return res;
        }
        out = correctValue;
        return Success;
    }
    static Napi::Value ToJS(Napi::Env env, const EnforceRangeInteger<T>& value) {
        return Converter<T>::ToJS(env, value.value);
    }
};

template <>
class Converter<UndefinedType> {
  public:
    static Result FromJS(Napi::Env, Napi::Value, UndefinedType&);
    static Napi::Value ToJS(Napi::Env, UndefinedType);
};

template <typename T>
class Converter<Interface<T>> {
  public:
    static Result FromJS(Napi::Env env, Napi::Value value, Interface<T>& out) {
        if (!value.IsObject()) {
            return Error("value is not object");
        }
        auto obj = value.As<Napi::Object>();
        if (!T::Unwrap(obj)) {
            return Error("object is not of the correct interface type");
        }
        out = Interface<T>(obj);
        return Success;
    }
    static Napi::Value ToJS(Napi::Env env, const Interface<T>& value) { return {env, value}; }
};

template <typename T>
class Converter<std::optional<T>> {
  public:
    static Result FromJS(Napi::Env env, Napi::Value value, std::optional<T>& out) {
        if (value.IsNull() || value.IsUndefined()) {
            out.reset();
            return Success;
        }
        T v{};
        auto res = Converter<T>::FromJS(env, value, v);
        if (!res) {
            return res;
        }
        out = std::move(v);
        return Success;
    }
    static Napi::Value ToJS(Napi::Env env, std::optional<T> value) {
        if (value.has_value()) {
            return Converter<T>::ToJS(env, value.value());
        }
        return env.Null();
    }
};

template <typename T>
class Converter<std::vector<T>> {
  public:
    static inline Result FromJS(Napi::Env env, Napi::Value value, std::vector<T>& out) {
        if (!value.IsArray()) {
            return Error("value is not an array");
        }
        auto arr = value.As<Napi::Array>();
        std::vector<T> vec(arr.Length());
        for (size_t i = 0; i < vec.size(); i++) {
            auto res = Converter<T>::FromJS(env, arr[static_cast<uint32_t>(i)], vec[i]);
            if (!res) {
                return res.Append("for array element ", i);
            }
        }
        out = std::move(vec);
        return Success;
    }
    static inline Napi::Value ToJS(Napi::Env env, const std::vector<T>& vec) {
        auto arr = Napi::Array::New(env, vec.size());
        for (size_t i = 0; i < vec.size(); i++) {
            arr.Set(static_cast<uint32_t>(i), Converter<T>::ToJS(env, vec[i]));
        }
        return arr;
    }
};

template <typename K, typename V>
class Converter<std::unordered_map<K, V>> {
  public:
    static inline Result FromJS(Napi::Env env, Napi::Value value, std::unordered_map<K, V>& out) {
        if (!value.IsObject()) {
            return Error("value is not an object");
        }
        auto obj = value.ToObject();
        auto keys = obj.GetPropertyNames();
        std::unordered_map<K, V> map(keys.Length());
        for (uint32_t i = 0; i < static_cast<uint32_t>(keys.Length()); i++) {
            K key{};
            V value{};
            auto key_res = Converter<K>::FromJS(env, keys[i], key);
            if (!key_res) {
                return key_res.Append("for object key");
            }
            auto value_res = Converter<V>::FromJS(env, obj.Get(keys[i]), value);
            if (!value_res) {
                return value_res.Append("for object value of key: ", key);
            }
            map[key] = value;
        }
        out = std::move(map);
        return Success;
    }
    static inline Napi::Value ToJS(Napi::Env env, std::unordered_map<K, V> value) {
        auto obj = Napi::Object::New(env);
        for (auto it : value) {
            obj.Set(Converter<K>::ToJS(env, it.first), Converter<V>::ToJS(env, it.second));
        }
        return obj;
    }
};

template <typename... TYPES>
class Converter<std::variant<TYPES...>> {
    template <typename TY>
    static inline Result TryFromJS(Napi::Env env, Napi::Value value, std::variant<TYPES...>& out) {
        TY v{};
        auto res = Converter<TY>::FromJS(env, value, v);
        if (!res) {
            return Error("no possible types matched");
        }
        out = std::move(v);
        return Success;
    }

    template <typename T0, typename T1, typename... TN>
    static inline Result TryFromJS(Napi::Env env, Napi::Value value, std::variant<TYPES...>& out) {
        if (TryFromJS<T0>(env, value, out)) {
            return Success;
        }
        return TryFromJS<T1, TN...>(env, value, out);
    }

  public:
    static inline Result FromJS(Napi::Env env, Napi::Value value, std::variant<TYPES...>& out) {
        return TryFromJS<TYPES...>(env, value, out);
    }
    static inline Napi::Value ToJS(Napi::Env env, std::variant<TYPES...> value) {
        return std::visit(
            [&](auto&& v) {
                using T = std::remove_cv_t<std::remove_reference_t<decltype(v)>>;
                return Converter<T>::ToJS(env, v);
            },
            value);
    }
};

template <typename T>
class Converter<Promise<T>> {
  public:
    static inline Result FromJS(Napi::Env env, Napi::Value, Promise<T>&) { UNIMPLEMENTED(env, {}); }
    static inline Napi::Value ToJS(Napi::Env, Promise<T> promise) { return promise; }
};

////////////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////////////

// FromJS() is a helper function which delegates to
// Converter<T>::FromJS()
template <typename T>
inline Result FromJS(Napi::Env env, Napi::Value value, T& out) {
    return Converter<T>::FromJS(env, value, out);
}

// FromJSOptional() is similar to FromJS(), but if 'value' is either null
// or undefined then 'out' is left unassigned.
template <typename T>
inline Result FromJSOptional(Napi::Env env, Napi::Value value, T& out) {
    if (value.IsNull() || value.IsUndefined()) {
        return Success;
    }
    return Converter<T>::FromJS(env, value, out);
}

// ToJS() is a helper function which delegates to Converter<T>::ToJS()
template <typename T>
inline Napi::Value ToJS(Napi::Env env, T&& value) {
    return Converter<std::remove_cv_t<std::remove_reference_t<T>>>::ToJS(env,
                                                                         std::forward<T>(value));
}

// DefaultedParameter can be used in the tuple parameter types passed to
// FromJS(const Napi::CallbackInfo& info, PARAM_TYPES& args), for parameters
// that have a default value. If the argument is omitted in the call, then
// DefaultedParameter::default_value will be assigned to
// DefaultedParameter::value.
template <typename T>
struct DefaultedParameter {
    T value;          // The argument value assigned by FromJS()
    T default_value;  // The default value if no argument supplied

    // Implicit conversion operator. Returns value.
    inline operator const T&() const { return value; }
};

// IsDefaultedParameter<T>::value is true iff T is of type DefaultedParameter.
template <typename T>
struct IsDefaultedParameter {
    static constexpr bool value = false;
};
template <typename T>
struct IsDefaultedParameter<DefaultedParameter<T>> {
    static constexpr bool value = true;
};

// FromJS() is a helper function for bulk converting the arguments of 'info'.
// PARAM_TYPES is a std::tuple<> describing the C++ function parameter types.
// Parameters may be of the templated DefaultedParameter type, in which case
// the parameter will default to the default-value if omitted.
template <typename PARAM_TYPES, int BASE_INDEX = 0>
inline Result FromJS(const Napi::CallbackInfo& info, PARAM_TYPES& args) {
    if constexpr (BASE_INDEX < std::tuple_size_v<PARAM_TYPES>) {
        using T = std::tuple_element_t<BASE_INDEX, PARAM_TYPES>;
        auto& value = info[BASE_INDEX];
        auto& out = std::get<BASE_INDEX>(args);
        if constexpr (IsDefaultedParameter<T>::value) {
            // Parameter has a default value.
            // Check whether the argument was provided.
            if (value.IsNull() || value.IsUndefined()) {
                // Use default value for this parameter
                out.value = out.default_value;
            } else {
                // Argument was provided
                auto res = FromJS(info.Env(), value, out.value);
                if (!res) {
                    return res;
                }
            }
        } else {
            // Parameter does not have a default value.
            auto res = FromJS(info.Env(), value, out);
            if (!res) {
                return res;
            }
        }
        // Convert the rest of the arguments
        return FromJS<PARAM_TYPES, BASE_INDEX + 1>(info, args);
    } else {
        return Success;
    }
}

}  // namespace wgpu::interop

#endif  // SRC_DAWN_NODE_INTEROP_CORE_H_
